/*****************************************************************************
 * $LastChangedDate: 2012-03-10 11:00:48 -0500 (Sat, 10 Mar 2012) $
 * @file
 * @author  Jim E. Brooks  http://www.palomino3d.org
 * @brief   Cache of 3D model files.
 * @verbatim
 *
 * Model scale and model offset:
 * -----------------------------
 * A 3D model is scaled by scaling its matrix.
 * The scale is computed such that is multiplies a 3D meter vector.
 * This means that a model's offset, which is defined in meters,
 * can be directly assigned to its matrix's origin.
 * Evidently, OSG must be multiplying the matrix origin by the matrix scale.
 *
 * Compressed (gzip) 3D model files can be loaded:
 * ---------------------------------------------
 * Compressed 3D models are read from a .gz file using "gzstream"
 * which is passed to osgDB::ReaderWriter::readNode(std::istream&).
 * A consequence is that OSG must be notified of all directories
 * that contains graphics assets referenced by the 3D model file.
 *
 * @endverbatim
 *//*
 * LEGAL:   COPYRIGHT (C) 2008 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE VERSION 2 (GPL 2).
 *****************************************************************************/

#define GRAPH_MODEL_CACHE_CC 1
#if VERBOSE
#define MODEL_CACHE_VERBOSE 1
#endif
#include <utility>
#include <vector>
#include <sstream>
#include "base/module.hh"
#include "base/conf.hh"
#include "base/file.hh"
#include "base/stream.hh"
#include "base/stream_zlib.hh"  // gzstream
using namespace base;
#include "math/module.hh"
#include "math/funcs.hh"
#include "math/matrix.hh"
#include "math/matrix_funcs.hh"
using namespace math;
#include "object/module.hh"
using namespace object;
#include "world/module.hh"
#include "world/conf.hh"
using namespace world;
#include "shader/module.hh"
#include "shader/conf.hh"
#include "shader/shader_model.hh"
using namespace shader;
#include "graph/module.hh"
#include "graph/osg.hh"
#include "graph/graph.hh"
#include "graph/model_cache.hh"

#include <osgDB/ReadFile>
#include <osgDB/Registry>
#include <osgDB/FileUtils>

namespace graph {

////////////////////////////////////////////////////////////////////////////////
//////////////////////////////  ModelCache  ////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

ModelCache* ModelCache::msInstance;  // there can be only one

/*****************************************************************************
 * ctor/dtor.
 *****************************************************************************/
ModelCache::ModelCache( void )
:   mModelMap()
{
    // Add subdirs of models/ to OSG's list of data directories.
    // Otherwise, OSG won't find textures etc referenced by 3D model files
    // which read indirectly via a C++ stream (zlib/gzip).
    osgDB::FilePathList dataPathList = osgDB::getDataFilePathList();
    const string modelDirName = GetDataDir() + string("/models");
    std::vector<string> filesInDir;  // includes subdirs
    bool subdirsExist = FilesInDir( modelDirName, filesInDir );
    if ( not subdirsExist or filesInDir.empty() )
        throw Exception( string("ModelCache: cannot find subdirs at ") + modelDirName );
    for ( uint i = 0; i < filesInDir.size(); ++i )
    {
        if ( filesInDir[i] == ".svn" )
            continue;

        // Is this a subdir (could be a file)?
        const string subdirName = modelDirName + string("/") + filesInDir[i];
        if ( IfDirExists( subdirName ) )
        {
            #if MODEL_CACHE_VERBOSE
            CLOG << "ModelCache: adding data dir " << subdirName << std::endl;
            #endif
            dataPathList.push_back( subdirName );
        }
    }
    osgDB::setDataFilePathList( dataPathList );
}

ModelCache::~ModelCache()
{
    // NOP
}

/*****************************************************************************
 * Load a cached 3D model according to physics parameters else THROW EXCEPTION.
 * This can load OSG archive files .osga.
 *
 * NOTE: The reason for the LoadModel*() subroutines are to solve the
 * chicken/egg problem caused by GeoVertex2WorldVertex() depending on FlatLand().
 * FlatLand() ctor calls the direct LoadModel() which doesn't use GeoVertex2WorldVertex().
 *
 * 3D model offset:
 * ----------------
 * Some 3D models need their origin to be moved for proper rotation.
 * The existence of a 3D model offset is handled by the function LoadModel()
 * which splices a transform node before it is passed to a Graph's ctor.
 * Thus, Object and Graph classes aren't aware.
 *
 * @param   modelFilename
 *          Or sub-pathname such as "p51/p51.ac"
 * @param   modelLength
 *          In meters.
 * @param   modelOffset
 *          To center the model (this is in METERS).
 * @param   rotationList
 *          To orient the model.
 * @param   loadCopy
 *          Pass ModelCache::LOAD_SHARED or LOAD_COPY.
 *          If false, return a possibly shared 3D model.
 *          If true, return an independent copy.
 *          For dynamic craft, nodes should be copies so that propellers etc
 *          can be rotated independently without affecting other craft.
 *****************************************************************************/
shptr<Graph>
ModelCache::LoadModel( const string&       modelFilename,  // sub-pathname
                       const Meter         modelLength,
                       const GeoVertex&    modelOffset,
                       const RotationList& rotationList,
                       const fp            modelScale,
                       const bool          loadCopy )
{
ASSERT( modelFilename != "" );
ASSERT( modelLength > Meter(0) );
ASSERT( modelScale > 0.0f );

    // Directly load 3D model into a transform node.
    RefPtr<osg::MatrixTransform> transform0 = LoadModelIntoTransform( modelFilename, loadCopy );

    // Apply a RotationList to orient a specific 3D model.
    Matrix m = transform0->getMatrix(); 
    rotationList.Apply( m );
    transform0->setMatrix( m );

    // Compute scale of 3D model.
    // Converting the model's length into graphical coordinates (sim)
    // accomplishes the scaling of the 3D model into world/planet.
    // Then adjust by the configured ObjectScale.
    // scale = sim/meter
    // Note: The scaled matrix transforms a 3D meters into graphical world space.
    ASSERT( WORLD_CONF.mObjectScale > 0.0f );
    fp scale1D = world::conv::Meters2Sim(modelLength) / modelLength.FP();
    scale1D *= WORLD_CONF.mObjectScale;
    scale1D *= modelScale;  // scale for this specific model
    MatrixScale( *transform0, Vector3(scale1D,scale1D,scale1D) );

    // Apply an offset to change the center of a specific 3D model.
    // SUBTLE: Offset in meters is used DIRECTLY.
    //         The reason is that the scaled matrix multiplies it.
    MatrixTranslate( *transform0, modelOffset.ToVector3() );

    // Modify 3D model for rendering by shaders.
    if ( SHADER_CONF.mEnableShaders )
        ShaderModel::ModifyModelForShaders( transform0.get() );

    // Return 3D model as a Graph.
    // Subtle: Transparently pass a transform node as a root node to Graph.
    // Graph will have two transform nodes but Graph class will be unaware.
    return new Graph( transform0.get(), FileBasename(modelFilename) );
}

/*****************************************************************************
 * Directly load a cached 3D model else THROW EXCEPTION.
 *****************************************************************************/
shptr<Graph>
ModelCache::LoadModel( const string& modelFilename,
                       const fp scale,
                       const bool loadCopy )  // sub-pathname
{
    RefPtr<osg::MatrixTransform> transform0 = LoadModelIntoTransform( modelFilename, loadCopy );
    MatrixScale( *transform0, Vector3(scale,scale,scale) );
    return new Graph( transform0.get(), FileBasename(modelFilename) );
}

/*****************************************************************************
 * Private subroutines.
 *****************************************************************************/
// The core of the cache.
RefPtr<osg::Node>
ModelCache::LoadModelIntoNode( const string& modelFilename, const bool loadCopy )  // sub-pathname
{
    if ( loadCopy )
    {
        // Bypass cache, load new copy.
#if MODEL_CACHE_VERBOSE
        CLOG << "ModelCache: loading copy of " << modelFilename << std::endl;
#endif
        return LoadModelIntoNode2( modelFilename );
    }
    else  // shared
    {
        // Was model loaded before?
        ModelMap::iterator iter = mModelMap.find( modelFilename );
        if ( iter == mModelMap.end() )
        {
            RefPtr0<osg::Node> modelNode = LoadModelIntoNode2( modelFilename );

            // Store in cache (by filename, not full pathname).
            mModelMap.insert( std::make_pair( modelFilename, modelNode ) );
#if MODEL_CACHE_VERBOSE
            CLOG << "ModelCache: caching " << modelFilename << std::endl;
#endif
            return modelNode;
        }
        else
        {
#if MODEL_CACHE_VERBOSE
            CLOG << "ModelCache: found " << modelFilename << std::endl;
#endif
            return iter->second;  // RefPtr<osg::Node>
        }
    }
}

// Directly load 3D file with error-checking.
RefPtr<osg::Node>
ModelCache::LoadModelIntoNode2( const string& modelFilename )  // sub-pathname
{
    // No, directly load 3D model from file.
    // Catenate 3D model directory with model filename
    const string pathname = string(base::GetDataDir()) + string("/") + string(modelFilename);
    RefPtr0<osg::Node> modelNode = ReadNodeFile( pathname );

    // Load error?
    if ( UX( modelNode == NULL ) )
    {
        std::ostringstream msg;
        msg << "ERROR: cannot load 3D model " << pathname << std::endl;
        if ( IfFileExists( pathname ) )
        {
            // The 3D model file exists, so probably OSG plugin (lib) failed to load.
            msg << "Ensure that LD_LIBRARY_PATH (DYLD_LIBRAY_PATH Mac) points to" << std::endl
                << "OSG plugin libraries: /usr/local/lib/osgPlugins-N.N.N" << std::endl;
        }
        else
        {
            msg << "Download terrain data file, or run from dir that has data/ subdir" << std::endl;
        }
        throw Exception( msg.str() );
    }

    return modelNode;
}

RefPtr<osg::MatrixTransform>
ModelCache::LoadModelIntoTransform( const string& modelFilename, const bool loadCopy )  // sub-pathname
{
    // Load model into a node.
    RefPtr<osg::Node> modelNode = LoadModelIntoNode( modelFilename, loadCopy );

    // Splice a transform node over the model's node.
    RefPtr<osg::MatrixTransform> transform0 = new osg::MatrixTransform;
    transform0->addChild( modelNode.get() );

    // Return transform node.
    return transform0;
}

/*****************************************************************************
 * Low-level function to read a compressed/uncompressed model file.
 *****************************************************************************/
RefPtr0<osg::Node>
ModelCache::ReadNodeFile( const string& pathname )
{
    // gzip compressed?
    string::size_type idx = pathname.find( ".gz" );
    if ( idx != string::npos )
    {
        // Use ZLIB to decompress the 3D file into a C++ string.
        igzstream compressedStream;
        compressedStream.open( pathname.c_str() );
        if ( not compressedStream.good() )
            throw Exception( string("ModelCache: gzstream failed ") + pathname );

        // For osgDB::getReaderWriterForExtension():
        // Change: ac.gz --> ac  (omit leading '.')
        string modelFileSuffix = pathname;
        if ( modelFileSuffix.size() <= 3 )
            throw Exception( string("ModelCache: cannot determine suffix ") + pathname );
        modelFileSuffix.erase( modelFileSuffix.size()-3, 3 );  // remove ".gz"
        idx = modelFileSuffix.rfind( "." );
        if ( idx == string::npos )
            throw Exception( string("ModelCache: cannot determine suffix[2] ") + pathname );
        modelFileSuffix.erase( 0, idx+1 );
        if ( modelFileSuffix.empty() )
            throw Exception( string("ModelCache: cannot determine suffix[3] ") + pathname );
        #if MODEL_CACHE_VERBOSE
        CLOG << "ModelCache: model file suffix ." << modelFileSuffix << std::endl;
        #endif

        // Pass gzstream to OSG.
        SafePtr<osgDB::ReaderWriter> readerWriter = osgDB::Registry::instance()->getReaderWriterForExtension( modelFileSuffix );
        if ( readerWriter == NULL )
            throw Exception( "ModelCache: getReaderWriterForExtension() failed" );
        osgDB::ReaderWriter::ReadResult res = readerWriter->readNode( compressedStream );
        if ( not res.validNode() )
            throw Exception( "ModelCache: readerWriter->readNode() failed" );
        return res.takeNode();
    }
    else
    {
        // Read uncompressed 3D file.
        // -- KLUDGE: Workaround for OSG cache bug:
        // -- Without CACHE_NONE, in OSG 3.0.1, this will have the bad side-effect (OSG BUG)
        // -- of causing OSG to try to load any files from the terrain .osga archive file,
        // -- preventing the loading of texture files etc.
        RefPtr<osgDB::ReaderWriter::Options> options = new osgDB::ReaderWriter::Options();
        options->setObjectCacheHint( osgDB::ReaderWriter::Options::CACHE_NONE );
        // -- KLUDGE: 
        return osgDB::readNodeFile( pathname, options );
    }
}

/*****************************************************************************
 * Flush the cache of 3D models.
 * Cache should be flushed before reloading the scene
 * when any 3D models were modified externally.
 *****************************************************************************/
void
ModelCache::Flush( void )
{
#if MODEL_CACHE_VERBOSE
    CLOG << "ModelCache: flushed cache" << std::endl;
#endif

    mModelMap.clear();
}

} // namespace graph
